package com.googlecode.gql4j;

import java.math.BigDecimal;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.TokenStream;

import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Query.SortDirection;
import com.google.appengine.api.users.User;
import com.google.appengine.repackaged.com.google.common.base.Preconditions;
import com.google.appengine.repackaged.com.google.common.collect.Lists;
import com.google.appengine.repackaged.com.google.common.collect.Maps;
import com.googlecode.gql4j.antlr.GQLLexer;
import com.googlecode.gql4j.antlr.GQLParser;

/**
 * @author Max Zhu (thebbsky@gmail.com)
 *
 */
public class GqlQuery {
	
	private Query query;
	
	private FetchOptions fetchOptions; 
	
	/**
	 * @param queryStr
	 * @param context
	 */
	public GqlQuery(String queryStr, Map<String, Object> context) {
		super();
		
		Preconditions.checkNotNull(queryStr);
		
		build(queryStr, context);
	}

	/**
	 * @param queryStr
	 * @param params
	 */
	public GqlQuery(String queryStr, Object ...params) {
		super();
		
		Preconditions.checkNotNull(queryStr);
		
		// evaluation context
		Map<String, Object> context = Maps.newHashMap();
		for (int i = 0 ; i < params.length ; i ++) {
			context.put(String.valueOf(i + 1), params[i]);
		}
		
		build(queryStr, context);
	}

	private void build(String queryStr, Map<String, Object> context) {
		ParseResult r = parse(queryStr);
		
		// from clause
		if (r.from == null) {
			this.query = new Query();
		} else {
			this.query = new Query(r.from.kind);
		}
		
		// select clause
		if (r.select.isKeyOnly()) {
			this.query.setKeysOnly();
		}
		
		// where clause
		if (r.where != null) {
			for (Condition c : r.where.conditions) {
				this.query.addFilter(c.propertyName, c.operator, c.e.evaluate(context));
			}
			
			// set ancester
			if (r.where.ancestor != null) {
				this.query.setAncestor(r.where.ancestor.ancestorKey(context));
			}
		}
		
		// order by
		if (r.orderBy != null) {
			for (OrderByItem o : r.orderBy.items) {
				this.query.addSort(o.propertyName, o.direction);
			}
		}
		
		// limit
		if (r.limit != null) {
			this.fetchOptions = FetchOptions.Builder.withLimit(r.limit.limit);
		}
		
		if (r.offset != null) {
			if (this.fetchOptions == null) {
				this.fetchOptions = FetchOptions.Builder.withDefaults();
			}
			
			this.fetchOptions.offset(r.offset.offset);
		}
	}
	
	static ParseResult parse(String queryStr) {		
		try {
			CharStream input = new ANTLRStringStream(queryStr);
			
			GQLLexer lexer = new GQLLexer(input);
			TokenStream tokens = new CommonTokenStream(lexer);
			GQLParser parser = new GQLParser(tokens);
			return parser.query().r;
		} catch (RecognitionException e) {
			throw new GqlQueryException("GQL syntax error");
		}
	}

	public Query query() {
		return query;
	}

	public FetchOptions fetchOptions() {
		if (fetchOptions == null) {
			fetchOptions = FetchOptions.Builder.withDefaults();
		}
		
		return fetchOptions;
	}

	public static class GqlQueryException extends RuntimeException {

		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		public GqlQueryException() {
			super();
		}

		public GqlQueryException(String message, Throwable cause) {
			super(message, cause);
		}

		public GqlQueryException(String message) {
			super(message);
		}

		public GqlQueryException(Throwable cause) {
			super(cause);
		}
	}
	
	public static class ParseResult {
		private Select select;
		
		private From from;
		
		private Where where;
		
		private OrderBy orderBy;
		
		private Limit limit;
		
		private Offset offset;

		public Select getSelect() {
			return select;
		}

		public ParseResult setSelect(Select select) {
			this.select = select;
			return this;
		}

		public From getFrom() {
			return from;
		}

		public ParseResult setFrom(From from) {
			this.from = from;
			return this;
		}

		public Where getWhere() {
			return where;
		}

		public ParseResult setWhere(Where where) {
			this.where = where;
			return this;
		}

		public OrderBy getOrderBy() {
			return orderBy;
		}

		public ParseResult setOrderBy(OrderBy orderBy) {
			this.orderBy = orderBy;
			return this;
		}

		public Limit getLimit() {
			return limit;
		}

		public ParseResult setLimit(Limit limit) {
			this.limit = limit;
			return this;
		}

		public Offset getOffset() {
			return offset;
		}

		public ParseResult setOffset(Offset offset) {
			this.offset = offset;
			return this;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((from == null) ? 0 : from.hashCode());
			result = prime * result + ((limit == null) ? 0 : limit.hashCode());
			result = prime * result + ((offset == null) ? 0 : offset.hashCode());
			result = prime * result + ((orderBy == null) ? 0 : orderBy.hashCode());
			result = prime * result + ((select == null) ? 0 : select.hashCode());
			result = prime * result + ((where == null) ? 0 : where.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ParseResult other = (ParseResult) obj;
			if (from == null) {
				if (other.from != null)
					return false;
			} else if (!from.equals(other.from))
				return false;
			if (limit == null) {
				if (other.limit != null)
					return false;
			} else if (!limit.equals(other.limit))
				return false;
			if (offset == null) {
				if (other.offset != null)
					return false;
			} else if (!offset.equals(other.offset))
				return false;
			if (orderBy == null) {
				if (other.orderBy != null)
					return false;
			} else if (!orderBy.equals(other.orderBy))
				return false;
			if (select == null) {
				if (other.select != null)
					return false;
			} else if (!select.equals(other.select))
				return false;
			if (where == null) {
				if (other.where != null)
					return false;
			} else if (!where.equals(other.where))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "ParseResult [select=" + select + ", from=" + from + ", where=" + where + ", orderBy=" + orderBy
					+ ", limit=" + limit + ", offset=" + offset + "]";
		}
	}
	
	public static class Select {		
		/**
		 * select item, only * and __key__ is allowed (case sensitive)
		 */
		private final boolean keyOnly;

		public Select(boolean keyOnly) {
			super();
			this.keyOnly = keyOnly;
		}
		
		public boolean isKeyOnly() {
			return this.keyOnly;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + (keyOnly ? 1231 : 1237);
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Select other = (Select) obj;
			if (keyOnly != other.keyOnly)
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "Select [keyOnly=" + keyOnly + "]";
		}
	}
	
	public static class From {
		private final String kind;

		public From(String kind) {
			super();
			this.kind = kind;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((kind == null) ? 0 : kind.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			From other = (From) obj;
			if (kind == null) {
				if (other.kind != null)
					return false;
			} else if (!kind.equals(other.kind))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "From [kind=" + kind + "]";
		}
	}
	
	public static class Where {
		private final List<Condition> conditions;
		
		private Ancestor ancestor;
		
		public Where() {
			conditions = Lists.newLinkedList();
		}
		
		public Where withCondition(Condition condition) {
			this.conditions.add(condition);
			return this;
		}
		
		public Where withAncestor(Evaluator e) {
			ancestor = new Ancestor(e);
			return this;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((ancestor == null) ? 0 : ancestor.hashCode());
			result = prime * result + ((conditions == null) ? 0 : conditions.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Where other = (Where) obj;
			if (ancestor == null) {
				if (other.ancestor != null)
					return false;
			} else if (!ancestor.equals(other.ancestor))
				return false;
			if (conditions == null) {
				if (other.conditions != null)
					return false;
			} else if (!conditions.equals(other.conditions))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "Where [conditions=" + conditions + ", ancestor=" + ancestor + "]";
		}
	}
	
	public static class Ancestor {

		private final Evaluator e;
		
		public Ancestor(Evaluator e) {
			super();
			this.e = e;
		}

		public Key ancestorKey(Map<String, Object> context) {
			Object val = e.evaluate(context);
			if (val instanceof Key) {
				return (Key)val;
			} else if (val instanceof Entity) {
				return ((Entity) val).getKey();
			} else {
				throw new GqlQueryException("Invalid GQL query string. ANCESTOR IS must be followed by Key or Entity");
			}
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((e == null) ? 0 : e.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Ancestor other = (Ancestor) obj;
			if (e == null) {
				if (other.e != null)
					return false;
			} else if (!e.equals(other.e))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "Ancestor [e=" + e + "]";
		}
	}
	
	public static class OrderBy {
		private final List<OrderByItem> items;
		
		public OrderBy() {
			items = Lists.newLinkedList();
		}
		
		public OrderBy withItem(OrderByItem item) {
			this.items.add(item);
			return this;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((items == null) ? 0 : items.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			OrderBy other = (OrderBy) obj;
			if (items == null) {
				if (other.items != null)
					return false;
			} else if (!items.equals(other.items))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "OrderBy [items=" + items + "]";
		}
	}
	
	public static class Limit {
		private final Integer limit;

		public Limit(Integer limit) {
			super();
			this.limit = limit;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((limit == null) ? 0 : limit.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Limit other = (Limit) obj;
			if (limit == null) {
				if (other.limit != null)
					return false;
			} else if (!limit.equals(other.limit))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "Limit [limit=" + limit + "]";
		}
	}
	
	public static class Offset {
		private final Integer offset;

		public Offset(Integer offset) {
			super();
			this.offset = offset;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((offset == null) ? 0 : offset.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Offset other = (Offset) obj;
			if (offset == null) {
				if (other.offset != null)
					return false;
			} else if (!offset.equals(other.offset))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "Offset [offset=" + offset + "]";
		}
	}
	
	/**
	 * where condition
	 * 
	 * @author Max Zhu (thebbsky@gmail.com)
	 *
	 */
	public static class Condition {
		private String propertyName;
		
		private FilterOperator operator;
		
		private Evaluator e;

		public Condition(String propertyName, FilterOperator operator, Evaluator e) {
			super();
			
			Preconditions.checkNotNull(propertyName);
			Preconditions.checkNotNull(operator);
			Preconditions.checkNotNull(e);
			
			this.propertyName = propertyName;
			this.operator = operator;
			this.e = e;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((e == null) ? 0 : e.hashCode());
			result = prime * result + ((operator == null) ? 0 : operator.hashCode());
			result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Condition other = (Condition) obj;
			if (e == null) {
				if (other.e != null)
					return false;
			} else if (!e.equals(other.e))
				return false;
			if (operator != other.operator)
				return false;
			if (propertyName == null) {
				if (other.propertyName != null)
					return false;
			} else if (!propertyName.equals(other.propertyName))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "Condition [propertyName=" + propertyName + ", operator=" + operator + ", e=" + e + "]";
		}
	}
	
	/**
	 * @author Max Zhu (thebbsky@gmail.com)
	 *
	 */
	public static class OrderByItem {
		
		private String propertyName;
		
		private SortDirection direction;

		public OrderByItem(String propertyName) {
			super();
			this.propertyName = propertyName;
			this.direction = SortDirection.ASCENDING;
		}
		
		public OrderByItem setDirection(boolean ascending) {
			this.direction = ascending ? SortDirection.ASCENDING : SortDirection.DESCENDING;
			return this;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((direction == null) ? 0 : direction.hashCode());
			result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			OrderByItem other = (OrderByItem) obj;
			if (direction != other.direction)
				return false;
			if (propertyName == null) {
				if (other.propertyName != null)
					return false;
			} else if (!propertyName.equals(other.propertyName))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "OrderByItem [propertyName=" + propertyName + ", direction=" + direction + "]";
		}
	}
	
	public static interface Evaluator {
		
		/**
		 * @param context
		 * @return
		 */
		public Object evaluate(Map<String, Object> context);
	}
	
	public static class NullEvaluator implements Evaluator {

		private static NullEvaluator singleton;
		
		public static synchronized NullEvaluator get() {
			if (singleton == null) {
				singleton = new NullEvaluator();
			}
			return singleton;
		}
		
		private NullEvaluator() {}
		
		@Override
		public Object evaluate(Map<String, Object> context) {
			return null;
		}
	}
	
	public static class DecimalEvaluator implements Evaluator {
		private final BigDecimal payload;
		
		public DecimalEvaluator(String strNumber) {
			payload = new BigDecimal(strNumber);
		}

		@Override
		public Object evaluate(Map<String, Object> context) {
			if ((double)payload.longValue() == payload.doubleValue()) {
				return this.payload.longValue();
			} else {
				return this.payload.doubleValue();
			}
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((payload == null) ? 0 : payload.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			DecimalEvaluator other = (DecimalEvaluator) obj;
			if (payload == null) {
				if (other.payload != null)
					return false;
			} else if (!payload.equals(other.payload))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "DecimalEvaluator [payload=" + payload + "]";
		}
	}
	
	public static class StringEvaluator implements Evaluator {
		private final String payload;
		
		public StringEvaluator(String rawText) {
			super();
			// remove single quote
			String withoutQuote = rawText.substring(1, rawText.length() - 1);
			
			// replace \' with '
			this.payload = withoutQuote.replace("\\'", "'");
		}

		@Override
		public Object evaluate(Map<String, Object> context) {
			return this.payload;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((payload == null) ? 0 : payload.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			StringEvaluator other = (StringEvaluator) obj;
			if (payload == null) {
				if (other.payload != null)
					return false;
			} else if (!payload.equals(other.payload))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "StringEvaluator [payload=" + payload + "]";
		}
	}
	
	public static class BooleanEvaluator implements Evaluator {
		private final Boolean payload;

		public BooleanEvaluator(String input) {
			this.payload = Boolean.valueOf(input);
		}
		
		@Override
		public Object evaluate(Map<String, Object> context) {
			return this.payload;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((payload == null) ? 0 : payload.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			BooleanEvaluator other = (BooleanEvaluator) obj;
			if (payload == null) {
				if (other.payload != null)
					return false;
			} else if (!payload.equals(other.payload))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "BooleanEvaluator [payload=" + payload + "]";
		}
	}
	
	public static class FunctionEvaluator implements Evaluator {
		
		public enum Type {
			DATETIME, 
			DATE,
			TIME, 
			KEY, 
			USER,
			GEOPT
		}
		
		private final Type type;
		
		private final List<Evaluator> ops;
		
		public FunctionEvaluator(String type, Evaluator ...ops) {
			this.type = Type.valueOf(type.toUpperCase());
			this.ops = Lists.newArrayList(ops);
		}
		
		public FunctionEvaluator(String type, List<Evaluator> ops) {
			this.type = Type.valueOf(type.toUpperCase());
			this.ops = ops;
		}
		
		public FunctionEvaluator withParam(Evaluator e) {
			this.ops.add(e);
			return this;
		}
		
		@Override
		public Object evaluate(Map<String, Object> context) {
			switch(this.type) {
			case DATETIME:
				return datetime(context);
			case DATE:
				return date(context);
			case TIME:
				return time(context);
			case GEOPT:
				return geopt(context);
			case KEY:
				return key(context);
			case USER:
				return user(context);
			default:
				// not supposed to happen
				throw new GqlQueryException("Invalid function type " + this.type);
			} 
		}

		private static final String DEFAULT_AUTH_DOMAIN = "gmail.com";
		
		private Object user(Map<String, Object> context) {
			if (this.ops.size() == 1) {
				Object val = this.ops.get(0).evaluate(context);
				if (val instanceof String) {					
					return new User((String)val, DEFAULT_AUTH_DOMAIN);
				} else {
					throw new GqlQueryException("Invalid GQL query string. Function key: invalid input");
				}	
			} else {
				throw new GqlQueryException("Invalid GQL query string. Function key: wrong number of arguments");
			}
		}

		private Object key(Map<String, Object> context) {
			if (this.ops.isEmpty()) {
				throw new GqlQueryException("Invalid GQL query string. Function key: wrong number of arguments");
			} else if (this.ops.size() == 1) {
				// KEY('encoded key')
				String keyString = (String) this.ops.get(0).evaluate(context);
				return KeyFactory.stringToKey(keyString);
			} else if (this.ops.size() % 2 == 0) {
				// KEY('kind', 'name'/ID [, 'kind', 'name'/ID...])
				try {
					Key key = null;
					Iterator<Evaluator> i = this.ops.iterator();
					while (i.hasNext()) {
						String kind = (String) i.next().evaluate(context);
						Object nameId = i.next().evaluate(context);
						
						if (nameId instanceof String) {
							key = KeyFactory.createKey(key, kind, (String)nameId);
						} else if (nameId instanceof Long) {
							key = KeyFactory.createKey(key, kind, (Long)nameId);
						}
					}
					
					return key;
				} catch (ClassCastException e) {
					throw new GqlQueryException("Invalid GQL query string. Function key: invalid input", e);					
				}
			} else {
				throw new GqlQueryException("Invalid GQL query string. Function key: wrong number of arguments");
			}
		}

		private Object geopt(Map<String, Object> context) {
			// TODO Auto-generated method stub
			return null;
		}

		private static DateFormat timeFmter = new SimpleDateFormat("HH:mm:ss");
		
		private Object time(Map<String, Object> context) {
			if (this.ops.size() == 1) {
				// TIME('HH:MM:SS')
				Object r = this.ops.get(0).evaluate(context);
				if (r instanceof String) {
					try {
						return timeFmter.parse((String) r);
					} catch (ParseException e) {
						throw new GqlQueryException("Invalid GQL query string. Function time: invalid input", e);
					}
				} else {
					throw new GqlQueryException("Invalid GQL query string. Function time: invalid input");
				}
			} else if (this.ops.size() == 3) {
				// TIME(hour, minute, second)
				try {
					int hour = ((Number)this.ops.get(0).evaluate(context)).intValue();
					int minute = ((Number)this.ops.get(1).evaluate(context)).intValue();
					int second = ((Number)this.ops.get(2).evaluate(context)).intValue();
					
					Calendar c = Calendar.getInstance();
					c.set(Calendar.HOUR, hour);
					c.set(Calendar.MINUTE, minute);
					c.set(Calendar.SECOND, second);
					return c.getTime();
				} catch (ClassCastException e) {
					throw new GqlQueryException("Invalid GQL query string. Function time: invalid input", e);
				}
			} else {
				throw new GqlQueryException("Invalid GQL query string. Function time: wrong number of arguments");
			}
		}

		private static DateFormat dateFmter = new SimpleDateFormat("yyyy-MM-dd");
		
		private Object date(Map<String, Object> context) {
			if (this.ops.size() == 1) {
				// DATE('YYYY-MM-DD')
				Object r = this.ops.get(0).evaluate(context);
				if (r instanceof String) {
					try {
						return dateFmter.parse((String) r);
					} catch (ParseException e) {
						throw new GqlQueryException("Invalid GQL query string. Function date: invalid input", e);
					}
				} else {
					throw new GqlQueryException("Invalid GQL query string. Function date: invalid input");
				}
			} else if (this.ops.size() == 3) {
				// DATE(year, month, day)
				try {
					int year = ((Number)this.ops.get(0).evaluate(context)).intValue();
					int month = ((Number)this.ops.get(1).evaluate(context)).intValue();
					int day = ((Number)this.ops.get(2).evaluate(context)).intValue();
					
					return createDate(year, month, day, 0, 0, 0);
				} catch (ClassCastException e) {
					throw new GqlQueryException("Invalid GQL query string. Function date: invalid input", e);
				}
			} else {
				throw new GqlQueryException("Invalid GQL query string. Function date: wrong number of arguments");
			}
		}
		
		private static DateFormat datetimeFmter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		
		private Object datetime(Map<String, Object> context) {
			if (this.ops.size() == 1) {
				// DATETIME('YYYY-MM-DD HH:MM:SS')
				Object r = this.ops.get(0).evaluate(context);
				if (r instanceof String) {
					try {
						return datetimeFmter.parse((String) r);
					} catch (ParseException e) {
						throw new GqlQueryException("Invalid GQL query string. Function datetime: invalid input", e);
					}
				} else {
					throw new GqlQueryException("Invalid GQL query string. Function datetime: invalid input");
				}
			} else if (this.ops.size() == 6) {
				// DATETIME(year, month, day, hour, minute, second)
				try {
					int year = ((Number)this.ops.get(0).evaluate(context)).intValue();
					int month = ((Number)this.ops.get(1).evaluate(context)).intValue();
					int day = ((Number)this.ops.get(2).evaluate(context)).intValue();
					int hour = ((Number)this.ops.get(3).evaluate(context)).intValue();
					int minute = ((Number)this.ops.get(4).evaluate(context)).intValue();
					int second = ((Number)this.ops.get(5).evaluate(context)).intValue();
					
					return createDate(year, month, day, hour, minute, second);
				} catch (ClassCastException e) {
					throw new GqlQueryException("Invalid GQL query string. Function datetime: invalid input", e);
				}
			} else {
				throw new GqlQueryException("Invalid GQL query string. Function datetime: wrong number of arguments");
			}
		}
		
		private Date createDate(int year, int month, int day, int hour, int minute, int second) {
			Calendar c = Calendar.getInstance();
			c.clear();
			c.set(Calendar.YEAR, year);
			c.set(Calendar.MONTH, month);
			c.set(Calendar.DATE, day);
			c.set(Calendar.HOUR, hour);
			c.set(Calendar.MINUTE, minute);
			c.set(Calendar.SECOND, second);
			return c.getTime();
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((ops == null) ? 0 : ops.hashCode());
			result = prime * result + ((type == null) ? 0 : type.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			FunctionEvaluator other = (FunctionEvaluator) obj;
			if (ops == null) {
				if (other.ops != null)
					return false;
			} else if (!ops.equals(other.ops))
				return false;
			if (type != other.type)
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "FunctionEvaluator [type=" + type + ", ops=" + ops + "]";
		}
	}
	
	public static class ParamEvaluator implements Evaluator {

		private final String paramName;
		
		public ParamEvaluator(String paramName) {
			super();
			this.paramName = paramName;
		}
		
		@Override
		public Object evaluate(Map<String, Object> context) {
			return context.get(paramName);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((paramName == null) ? 0 : paramName.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ParamEvaluator other = (ParamEvaluator) obj;
			if (paramName == null) {
				if (other.paramName != null)
					return false;
			} else if (!paramName.equals(other.paramName))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "ParamEvaluator [paramName=" + paramName + "]";
		}
	}
	
	public static class ListEvaluator implements Evaluator {
		
		private final List<Evaluator> evaluators;
		
		public ListEvaluator(Evaluator ...evaluators) {
			super();
			this.evaluators = Lists.newArrayList(evaluators);
		}
		
		public ListEvaluator(List<Evaluator> evaluators) {
			super();
			this.evaluators = evaluators;
		}

		@Override
		public Object evaluate(Map<String, Object> context) {
			List<Object> result = new ArrayList<Object>(evaluators.size());
			for (Evaluator e : evaluators) {
				result.add(e.evaluate(context));
			}
			return result;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((evaluators == null) ? 0 : evaluators.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ListEvaluator other = (ListEvaluator) obj;
			if (evaluators == null) {
				if (other.evaluators != null)
					return false;
			} else if (!evaluators.equals(other.evaluators))
				return false;
			return true;
		}

		@Override
		public String toString() {
			return "ListEvaluator [evaluators=" + evaluators + "]";
		}
	}
}
